#include "config.h"

#include <stdio.h>
#include <string.h>
#define _USE_MATH_DEFINES
#include <math.h>
#include <GL/glew.h>
#include "vmath/vmath.h"
#include "vr.h"
#include "vr_impl.h"
#include "vr_sdr.h"

#ifndef WIN32
#include <alloca.h>
#else
#include <malloc.h>
#endif

static void init_ctx();
static bool init_ovr();
static bool init_sdr();

VRContext vr_ctx;
static unsigned int sdrprog;

extern "C" int vr_init(enum vr_init_mode mode)
{
	init_ctx();

	if(!init_ovr()) {
		return -1;
	}

	if(!init_sdr()) {
		return -1;
	}

	return 0;
}

extern "C" void vr_shutdown(void)
{
	delete [] vr_ctx.info.display;
#ifdef USE_LIBOVR
	delete vr_ctx.ovr_sfusion;
	//System::Destroy();
#endif

	memset(&vr_ctx, 0, sizeof vr_ctx);
}

static void init_ctx()
{
	vr_ctx.info.width = 1280;
	vr_ctx.info.height = 800;
	vr_ctx.info.fov = M_PI / 2.0;
	vr_ctx.info.aspect = (float)vr_ctx.info.width / (float)vr_ctx.info.height;
	vr_ctx.info.ipd = 0.035;
	vr_ctx.info.scale = 1.0;
}

static bool init_ovr()
{
#ifdef USE_LIBOVR
	LogMaskConstants log_level = LogMask_All;
	// initialize Oculus SDK
	const char *logenv = getenv("VR_LOGLEVEL");
	if(logenv) {
		switch(atoi(logenv)) {
		case 0:
			log_level = LogMask_None;
			break;
		case 1:
			log_level = LogMask_Regular;
			break;
		case 2:
		default:
			log_level = LogMask_All;
			break;
		}
	}

	System::Init(Log::ConfigureDefaultLog(log_level));
	if(!(vr_ctx.ovr_devman = DeviceManager::Create())) {
		fprintf(stderr, "failed to create OVR device manager\n");
		return false;
	}

	// create the display device
	HMDInfo info;
	if(!(vr_ctx.ovr_hmd_dev = vr_ctx.ovr_devman->EnumerateDevices<HMDDevice>().CreateDevice())) {
		fprintf(stderr, "no oculus rift devices found\n");
	} else {
		if(vr_ctx.ovr_hmd_dev->GetDeviceInfo(&info)) {
			printf("oculus device info:\n");
			printf("  name: %s\n", info.DisplayDeviceName);
			printf("  ipd: %f\n", info.InterpupillaryDistance);
			printf("  distortion: %f %f %f %f\n", info.DistortionK[0],
					info.DistortionK[1], info.DistortionK[2], info.DistortionK[3]);
		}

		// calculate and store viewing parameters
		vr_ctx.info.width = info.HResolution;
		vr_ctx.info.height = info.VResolution;
		vr_ctx.info.aspect = (float)vr_ctx.info.width / (float)vr_ctx.info.height;

		vr_ctx.info.ipd = info.InterpupillaryDistance;
		for(int i=0; i<4; i++) {
			vr_ctx.info.distort[i] = info.DistortionK[i];
		}

		Util::Render::StereoConfig stereohelp;
		stereohelp.SetFullViewport(Util::Render::Viewport(0, 0, vr_ctx.info.width, vr_ctx.info.height));
		stereohelp.SetStereoMode(Util::Render::Stereo_LeftRight_Multipass);
		stereohelp.SetHMDInfo(info);
		stereohelp.SetDistortionFitPointVP(-1.0, 0.0);

		vr_ctx.info.scale = stereohelp.GetDistortionScale();

		float vhalfsz = vr_ctx.info.scale * info.VScreenSize * 0.5;
		vr_ctx.info.fov = 2.0 * atan(vhalfsz / info.EyeToScreenDistance);

		vr_ctx.info.lens_center_offset = 0.5 - info.LensSeparationDistance / info.HScreenSize;

		// calculate center of projection shift to match the lens positions
		float center_dist_meters = info.HScreenSize * 0.25;
		float proj_shift = center_dist_meters - info.LensSeparationDistance * 0.5;
		vr_ctx.info.proj_center_offset = 4.0 * proj_shift / info.HScreenSize;

		// grab the display info
		vr_ctx.info.display = new char[strlen(info.DisplayDeviceName) + 1];
		strcpy(vr_ctx.info.display, info.DisplayDeviceName);

		vr_ctx.info.display_xoffs = info.DesktopX;
		vr_ctx.info.display_yoffs = info.DesktopY;

		printf("display: \"%s\" offset: %+d %+d\n", vr_ctx.info.display,
				vr_ctx.info.display_xoffs, vr_ctx.info.display_yoffs);
	}

	// get the sensor device
	if(vr_ctx.ovr_hmd_dev) {
		if(!(vr_ctx.ovr_sensor_dev = vr_ctx.ovr_hmd_dev->GetSensor())) {
			fprintf(stderr, "failed to get oculus sensor device\n");
		}
	} else {
		if(!(vr_ctx.ovr_sensor_dev = vr_ctx.ovr_devman->EnumerateDevices<SensorDevice>().CreateDevice())) {
			fprintf(stderr, "failed to get oculus sensor device\n");
		}
	}

	if(vr_ctx.ovr_sensor_dev) {
		SensorInfo sinfo;
		if(vr_ctx.ovr_sensor_dev->GetDeviceInfo(&sinfo)) {
			printf("oculus sensor device info:\n");
			printf("  name: %s\n", sinfo.ProductName);
		}

		vr_ctx.ovr_sfusion = new SensorFusion;
		vr_ctx.ovr_sfusion->AttachToSensor(vr_ctx.ovr_sensor_dev);
	}
#endif	// USE_LIBOVR
	return true;
}

#undef EXTERNAL_SHADER

static bool init_sdr()
{
	int status;

#ifdef EXTERNAL_SHADER
	FILE *fp = fopen("sdr/sdr.glsl", "rb");
	if(!fp) {
		perror("failed to load sdr.glsl");
		return false;
	}
	fseek(fp, 0, SEEK_END);
	long sz = ftell(fp);
	rewind(fp);

	char *buf = (char*)alloca(sz + 1);
	fread(buf, 1, sz, fp);
	buf[sz] = 0;
	sdr_src = buf;

	fclose(fp);
#endif

	unsigned int sdr = glCreateShader(GL_FRAGMENT_SHADER);
	glShaderSource(sdr, 1, &sdr_src, 0);
	glCompileShader(sdr);
	glGetShaderiv(sdr, GL_COMPILE_STATUS, &status);
	if(!status) {
		fprintf(stderr, "failed to compile distortion shader\n");

		int loglen;
		glGetShaderiv(sdr, GL_INFO_LOG_LENGTH, &loglen);

		if(loglen > 0) {
			char *log = (char*)alloca(loglen);
			glGetShaderInfoLog(sdr, loglen, &loglen, log);
			fprintf(stderr, "%s\n", log);
		}

		return false;
	}

	sdrprog = glCreateProgram();
	glAttachShader(sdrprog, sdr);
	glLinkProgram(sdrprog);
	if(!status) {
		fprintf(stderr, "failed to link distortion shader program\n");
		glDeleteShader(sdr);
		return false;
	}

	int loc;

	glUseProgram(sdrprog);

	if((loc = glGetUniformLocation(sdrprog, "tex")) != -1) {
		glUniform1i(loc, 0);
	}
	if((loc = glGetUniformLocation(sdrprog, "lens_center_offset")) != -1) {
		glUniform1f(loc, 0);
	}
	if((loc = glGetUniformLocation(sdrprog, "scr_center")) != -1) {
		glUniform2f(loc, 0, 0);
	}
	if((loc = glGetUniformLocation(sdrprog, "scale")) != -1) {
		glUniform1f(loc, vr_ctx.info.scale);
	}
	if((loc = glGetUniformLocation(sdrprog, "aspect")) != -1) {
		printf("setting aspect: %f\n", vr_ctx.info.aspect / 2.0);
		glUniform1f(loc, vr_ctx.info.aspect / 2.0);
	}
	if((loc = glGetUniformLocation(sdrprog, "scale_in")) != -1) {
		glUniform2f(loc, 1, 1);
	}
	if((loc = glGetUniformLocation(sdrprog, "dist_factors")) != -1) {
		glUniform4f(loc, vr_ctx.info.distort[0], vr_ctx.info.distort[1],
				vr_ctx.info.distort[2], vr_ctx.info.distort[3]);
	}

	return true;
}

extern "C" const char *vr_get_display_name(void)
{
	return vr_ctx.info.display;
}

extern "C" void vr_get_display_pos(int *xptr, int *yptr)
{
	*xptr = vr_ctx.info.display_xoffs;
	*yptr = vr_ctx.info.display_yoffs;
}

extern "C" int vr_get_width(void)
{
	return vr_ctx.info.width;
}

extern "C" int vr_get_height(void)
{
	return vr_ctx.info.height;
}

extern "C" float vr_get_fov(void)
{
	return vr_ctx.info.fov;
}

extern "C" float vr_get_aspect(void)
{
	return vr_ctx.info.aspect;
}

extern "C" void vr_set_eyedist(float ipd)
{
	vr_ctx.info.ipd = ipd;
}

extern "C" float vr_get_eyedist(void)
{
	return vr_ctx.info.ipd;
}

extern "C" void vr_set_distort(const float *coef)
{
	memcpy(vr_ctx.info.distort, coef, sizeof vr_ctx.info.distort);
}

extern "C" void vr_get_distort(float *coef)
{
	memcpy(coef, vr_ctx.info.distort, sizeof vr_ctx.info.distort);
}

extern "C" void vr_set_prediction_sec(float dt)
{
#ifdef USE_LIBOVR
	vr_ctx.ovr_sfusion->SetPrediction(dt);
#endif
}

extern "C" float vr_get_prediction_sec(void)
{
#ifdef USE_LIBOVR
	return vr_ctx.ovr_sfusion->GetPredictionDelta();
#endif
}

extern "C" void vr_get_view_matrix(float *res, int eye)
{
	// TODO
}

extern "C" void vr_get_proj_matrix(float *res, int eye)
{
#ifdef USE_LIBOVR
	static float eye_scale[] = {0.0, 1.0, -1.0};

	Matrix4f proj = Matrix4f::PerspectiveRH(vr_ctx.info.fov, vr_ctx.info.aspect / 2.0, 0.3, 1000.0);
	proj = Matrix4f::Translation(vr_ctx.info.proj_center_offset * eye_scale[eye], 0, 0) * proj;

	memcpy(res, proj.M[0], 16 * sizeof(float));
#else
	Matrix4x4 proj;
	proj.set_perspective(vr_ctx.info.fov, vr_ctx.info.aspect, 0.3, 1000.0);
	proj.transpose();
	memcpy(res, proj[0], 16 * sizeof(float));
#endif
}

extern "C" void vr_get_translation(float *offs)
{
	// current oculus devkit doesn't do translation
	offs[0] = offs[1] = offs[2] = 0.0f;
}

extern "C" void vr_get_rotation(float *quat)
{
#ifdef USE_LIBOVR
	Quatf oq = vr_ctx.ovr_sfusion->GetPredictedOrientation();
	quat[0] = oq.x;
	quat[1] = oq.y;
	quat[2] = oq.z;
	quat[3] = oq.w;
#else
	quat[0] = quat[1] = quat[2] = 0.0;
	quat[3] = 1.0;
#endif
}

extern "C" void vr_get_rotation_euler(float *euler)
{
#ifdef USE_LIBOVR
	Quatf oq = vr_ctx.ovr_sfusion->GetPredictedOrientation();
	oq.GetEulerAngles<Axis_Y, Axis_X, Axis_Z>(euler + 1, euler, euler + 2);
#else
	euler[0] = euler[1] = euler[2] = 0.0;
#endif
}

extern "C" void vr_draw_eye(int eye, unsigned int tex, float tex_scale_x, float tex_scale_y)
{
	static const float rects[3][4] = {
		{-1, -1, 1, 1},
		{-1, -1, 0, 1},
		{0, -1, 1, 1}
	};
	static const float offs_scale[3] = {0.0, -1.0, 1.0};

	glPushAttrib(GL_ENABLE_BIT);
	glDisable(GL_DEPTH_TEST);
	glDisable(GL_LIGHTING);
	glEnable(GL_TEXTURE_2D);

	glMatrixMode(GL_MODELVIEW);
	glPushMatrix();
	glLoadIdentity();

	glMatrixMode(GL_PROJECTION);
	glPushMatrix();
	glLoadIdentity();

	glUseProgram(sdrprog);

	if(sdrprog) {
		int loc;
		if((loc = glGetUniformLocation(sdrprog, "lens_center_offset")) != -1) {
			float offset = vr_ctx.info.lens_center_offset * offs_scale[eye];
			glUniform1f(loc, offset);
		}

		if((loc = glGetUniformLocation(sdrprog, "tex_scale")) != -1) {
			glUniform2f(loc, tex_scale_x, tex_scale_y);
		}
	}

	glBindTexture(GL_TEXTURE_2D, tex);
	glBegin(GL_QUADS);
	glColor4f(1, 1, 1, 1);
	glTexCoord2f(0, 0); glVertex2f(rects[eye][0], rects[eye][1]);
	glTexCoord2f(1, 0); glVertex2f(rects[eye][2], rects[eye][1]);
	glTexCoord2f(1, 1); glVertex2f(rects[eye][2], rects[eye][3]);
	glTexCoord2f(0, 1); glVertex2f(rects[eye][0], rects[eye][3]);
	glEnd();

	glUseProgram(0);

	glPopMatrix();
	glMatrixMode(GL_MODELVIEW);
	glPopMatrix();

	glPopAttrib();
}
